/*
Copyright (C) 2011 The University of Michigan

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

Please send inquiries to powertutor@umich.edu
 */

package vn.cybersoft.obs.andriod.batterystats2.components;

import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;

import vn.cybersoft.obs.andriod.batterystats2.phone.PhoneConstants;
import vn.cybersoft.obs.andriod.batterystats2.service.IterationData;
import vn.cybersoft.obs.andriod.batterystats2.service.PowerData;
import vn.cybersoft.obs.andriod.batterystats2.util.Recycler;
import vn.cybersoft.obs.andriod.batterystats2.util.SystemInfo;
import android.content.Context;
import android.net.wifi.WifiManager;
import android.os.SystemClock;
import android.util.Log;
import android.util.SparseArray;

public class Wifi extends PowerComponent {
	public static class WifiData extends PowerData {
		private static Recycler<WifiData> recycler = new Recycler<WifiData>();

		public static WifiData obtain() {
			WifiData result = recycler.obtain();
			if(result != null) return result;
			return new WifiData();
		}

		@Override
		public void recycle() {
			recycler.recycle(this);
		}

		public boolean wifiOn;
		public double packets;
		public long uplinkBytes;
		public long downlinkBytes;
		public double uplinkRate;
		public double linkSpeed;
		public int powerState;

		private WifiData() {
		}

		public void init(double packets, long uplinkBytes, long downlinkBytes,
				double uplinkRate, double linkSpeed, int powerState) {
			wifiOn = true;
			this.packets = packets;
			this.uplinkBytes = uplinkBytes;
			this.downlinkBytes = downlinkBytes;
			this.uplinkRate = uplinkRate;
			this.linkSpeed = linkSpeed;
			this.powerState = powerState;
		}

		public void init() {
			wifiOn = false;
		}

		public void writeLogDataInfo(OutputStreamWriter out) throws IOException {
			StringBuilder res = new StringBuilder();
			res.append("Wifi-on ").append(wifiOn).append("\n");
			if(wifiOn) {
				res.append("Wifi-packets ").append((long)Math.round(packets))
				.append("\nWifi-uplinkBytes ").append(uplinkBytes)
				.append("\nWifi-downlinkBytes ").append(downlinkBytes)
				.append("\nWifi-uplink ").append((long)Math.round(uplinkRate))
				.append("\nWifi-speed ").append((long)Math.round(linkSpeed))
				.append("\nWifi-state ").append(Wifi.POWER_STATE_NAMES[powerState])
				.append("\n");
			}
			out.write(res.toString());
		}
	}

	public static final int POWER_STATE_LOW = 0;
	public static final int POWER_STATE_HIGH = 1;
	public static final String[] POWER_STATE_NAMES = {"LOW", "HIGH"};

	private static final String TAG = "Wifi";

	private PhoneConstants phoneConstants;
	private WifiManager wifiManager;
	private SystemInfo sysInfo;

	private long lastLinkSpeed;
	private int[] lastUids;
	private WifiStateKeeper wifiState;
	private SparseArray<WifiStateKeeper> uidStates;

	private String transPacketsFile;
	private String readPacketsFile;
	private String transBytesFile;
	private String readBytesFile;
	private File uidStatsFolder;

	public Wifi(Context context, PhoneConstants phoneConstants) {
		this.phoneConstants = phoneConstants;
		wifiManager = (WifiManager)context.getSystemService(Context.WIFI_SERVICE);
		sysInfo = SystemInfo.getInstance();

		/* Try to grab the interface name.  If we can't find it will take a wild
		 * stab in the dark.
		 */
		String interfaceName = SystemInfo.getInstance().getProperty("wifi.interface");
		if(interfaceName == null) interfaceName = "eth0";

		lastLinkSpeed = -1;
		wifiState = new WifiStateKeeper(phoneConstants.wifiHighLowTransition(),
				phoneConstants.wifiLowHighTransition());
		uidStates = new SparseArray<WifiStateKeeper>();
		transPacketsFile = "/sys/devices/virtual/net/" +
				interfaceName + "/statistics/tx_packets";
		readPacketsFile = "/sys/devices/virtual/net/" +
				interfaceName + "/statistics/rx_packets";
		transBytesFile = "/sys/devices/virtual/net/" +
				interfaceName + "/statistics/tx_bytes";
		readBytesFile = "/sys/devices/virtual/net/" +
				interfaceName + "/statistics/rx_bytes";
		uidStatsFolder = new File("/proc/uid_stat");
	}

	@Override
	public IterationData calculateIteration(long iteration) {
		IterationData result = IterationData.obtain();

		int wifiStateFlag = wifiManager.getWifiState();
		if(wifiStateFlag != WifiManager.WIFI_STATE_ENABLED &&
				wifiStateFlag != WifiManager.WIFI_STATE_DISABLING) {
			/* We need to allow the real iterface state keeper to reset it's state
			 * so that the next update it knows it's coming back from an off state.
			 * We also need to clear all the uid information.
			 */
			wifiState.interfaceOff();
			uidStates.clear();
			lastLinkSpeed = -1;

			WifiData data = WifiData.obtain();
			data.init();
			result.setPowerData(data);
			return result;
		}

		long transmitPackets = sysInfo.readLongFromFile(transPacketsFile);
		long receivePackets = sysInfo.readLongFromFile(readPacketsFile);
		long transmitBytes = sysInfo.readLongFromFile(transBytesFile);
		long receiveBytes = sysInfo.readLongFromFile(readBytesFile);
		if(transmitPackets == -1 || receivePackets == -1 ||
				transmitBytes == -1 || receiveBytes == -1) {
			/* Couldn't read interface data files. */
			Log.w(TAG, "Failed to read packet and byte counts from wifi interface");
			return result;
		}

		/* Update the link speed every 15 seconds as pulling the WifiInfo structure
		 * from WifiManager is a little bit expensive.  This isn't really something
		 * that is likely to change very frequently anyway.
		 */
		if(iteration % 15 == 0 || lastLinkSpeed == -1) {
			lastLinkSpeed = wifiManager.getConnectionInfo().getLinkSpeed();
		}
		double linkSpeed = lastLinkSpeed;

		if(wifiState.isInitialized()) {
			wifiState.updateState(transmitPackets, receivePackets,
					transmitBytes, receiveBytes);
			WifiData data = WifiData.obtain();
			data.init(wifiState.getPackets(), wifiState.getUplinkBytes(),
					wifiState.getDownlinkBytes(), wifiState.getUplinkRate(),
					linkSpeed, wifiState.getPowerState());
			result.setPowerData(data);
		} else {
			wifiState.updateState(transmitPackets, receivePackets,
					transmitBytes, receiveBytes);
		}

		lastUids = sysInfo.getUids(lastUids);
		if(lastUids != null) for(int uid : lastUids) {
			if(uid == -1) {
				continue;
			}
			try {
				WifiStateKeeper uidState = uidStates.get(uid);
				if(uidState == null) {
					uidState = new WifiStateKeeper(phoneConstants.wifiHighLowTransition(),
							phoneConstants.wifiLowHighTransition());
					uidStates.put(uid, uidState);
				}

				if(!uidState.isStale()) {
					/* We use a huerstic here so that we don't poll for uids that haven't
					 * had much activity recently.
					 */
					 continue;
				}

				/* These read operations are the expensive part of polling. */
				receiveBytes = sysInfo.readLongFromFile(
						"/proc/uid_stat/" + uid + "/tcp_rcv");
				transmitBytes = sysInfo.readLongFromFile(
						"/proc/uid_stat/" + uid + "/tcp_snd");

				if(receiveBytes == -1 || transmitBytes == -1) {
					Log.w(TAG, "Failed to read uid read/write byte counts");
				} else if(uidState.isInitialized()) {
					/* We only have information about bytes received but what we really
					 * want is the number of packets received so we just have to
					 * estimate it.
					 */
					long deltaTransmitBytes = transmitBytes - uidState.getTransmitBytes();
					long deltaReceiveBytes = receiveBytes - uidState.getReceiveBytes();
					long estimatedTransmitPackets = (long)Math.round(deltaTransmitBytes /
							wifiState.getAverageTransmitPacketSize());
					long estimatedReceivePackets = (long)Math.round(deltaReceiveBytes /
							wifiState.getAverageReceivePacketSize());
					if(deltaTransmitBytes > 0 && estimatedTransmitPackets == 0) {
						estimatedTransmitPackets = 1;
					}
					if(deltaReceiveBytes > 0 && estimatedReceivePackets == 0) {
						estimatedReceivePackets = 1;
					}

					boolean active = transmitBytes != uidState.getTransmitBytes() ||
							receiveBytes != uidState.getReceiveBytes();
					uidState.updateState(
							uidState.getTransmitPackets() + estimatedTransmitPackets,
							uidState.getReceivePackets() + estimatedReceivePackets,
							transmitBytes, receiveBytes);

					if(active) {
						WifiData uidData = WifiData.obtain();
						uidData.init(uidState.getPackets(), uidState.getUplinkBytes(),
								uidState.getDownlinkBytes(), uidState.getUplinkRate(),
								linkSpeed, uidState.getPowerState());
						result.addUidPowerData(uid, uidData);
					}
				} else {
					uidState.updateState(0, 0, transmitBytes, receiveBytes);
				}
			} catch(NumberFormatException e) {
				Log.w(TAG, "Non-uid files in /proc/uid_stat");
			}
		}

		return result;
	}

	private static class WifiStateKeeper {
		private long lastTransmitPackets;
		private long lastReceivePackets;
		private long lastTransmitBytes;
		private long lastReceiveBytes;
		private long lastTime;

		private int powerState;
		private double lastPackets;
		private double lastUplinkRate;
		private double lastAverageTransmitPacketSize;
		private double lastAverageReceivePacketSize;

		private long deltaUplinkBytes;
		private long deltaDownlinkBytes;

		private double highLowTransition;
		private double lowHighTransition;

		private long inactiveTime;

		public WifiStateKeeper(double highLowTransition, double lowHighTransition) {
			this.highLowTransition = highLowTransition;
			this.lowHighTransition = lowHighTransition;
			lastTransmitPackets = lastReceivePackets = lastTransmitBytes =
					lastTime = -1;
			powerState = POWER_STATE_LOW;
			lastPackets = lastUplinkRate = 0;
			lastAverageTransmitPacketSize = 1000;
			lastAverageReceivePacketSize = 1000;
			inactiveTime = 0;
		}

		public void interfaceOff() {
			lastTime = SystemClock.elapsedRealtime();
			powerState = POWER_STATE_LOW;
		}

		public boolean isInitialized() {
			return lastTime != -1;
		}

		public void updateState(long transmitPackets, long receivePackets,
				long transmitBytes, long receiveBytes) {
			long curTime = SystemClock.elapsedRealtime();
			if(lastTime != -1 && curTime > lastTime) {
				double deltaTime = curTime - lastTime;
				lastUplinkRate = (transmitBytes - lastTransmitBytes) / 1024.0 *
						7.8125 / deltaTime;
				lastPackets = receivePackets + transmitPackets -
						lastReceivePackets - lastTransmitPackets;
				deltaUplinkBytes = transmitBytes - lastTransmitBytes;
				deltaDownlinkBytes = receiveBytes - lastReceiveBytes;
				if(transmitPackets != lastTransmitPackets) {
					lastAverageTransmitPacketSize = 0.9 * lastAverageTransmitPacketSize +
							0.1 * (transmitBytes - lastTransmitBytes) /
							(transmitPackets - lastTransmitPackets);
				}
				if(receivePackets != lastReceivePackets) {
					lastAverageReceivePacketSize = 0.9 * lastAverageReceivePacketSize +
							0.1 * (receiveBytes - lastReceiveBytes) /
							(receivePackets - lastReceivePackets);
				}

				if(receiveBytes != lastReceiveBytes ||
						transmitBytes != lastTransmitBytes) {
					inactiveTime = 0;
				} else {
					inactiveTime += curTime - lastTime;
				}

				if(lastPackets < highLowTransition) {
					powerState = POWER_STATE_LOW;
				} else if(lastPackets > lowHighTransition) {
					powerState = POWER_STATE_HIGH;
				}
			}
			lastTime = curTime;
			lastTransmitPackets = transmitPackets;
			lastReceivePackets = receivePackets;
			lastTransmitBytes = transmitBytes;
			lastReceiveBytes = receiveBytes;
		}

		public int getPowerState() {
			return powerState;
		}

		public double getPackets() {
			return lastPackets;
		}

		public long getUplinkBytes() {
			return deltaUplinkBytes;
		}

		public long getDownlinkBytes() {
			return deltaDownlinkBytes;
		}

		public double getUplinkRate() {
			return lastUplinkRate;
		}

		public double getAverageTransmitPacketSize() {
			return lastAverageTransmitPacketSize;
		}

		public double getAverageReceivePacketSize() {
			return lastAverageReceivePacketSize;
		}

		public long getTransmitPackets() {
			return lastTransmitPackets;
		}

		public long getReceivePackets() {
			return lastReceivePackets;
		}

		public long getTransmitBytes() {
			return lastTransmitBytes;
		}

		public long getReceiveBytes() {
			return lastReceiveBytes;
		}

		/* The idea here is that we don't want to have to read uid information
		 * every single iteration for each uid as it just takes too long.  So here
		 * we are designing a hueristic that helps us avoid polling for too many
		 * uids.
		 */
		 public boolean isStale() {
			 long curTime = SystemClock.elapsedRealtime();
			 return curTime - lastTime > (long)Math.min(10000, inactiveTime);
		 }
	}

	private long readLongFromFile(String filePath) {
		return sysInfo.readLongFromFile(filePath);
	}

	@Override
	public boolean hasUidInformation() {
		return uidStatsFolder.exists();
	}

	@Override
	public String getComponentName() {
		return "Wifi";
	}
}
